//
//  encfsvault.m
//  EncfsVault
//
//  Created by Stephen Baker on 10/29/07.
//  Copyright 2007, 2008 Stephen Baker (ChuckKnowsBest.com). All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//      http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include <Security/Authorization.h>
#include <Security/AuthorizationTags.h>
#include <Security/AuthorizationPlugin.h>

#include <stdlib.h>
#include <string.h>
#include <syslog.h>

/* Tiger SDK does not define X_FILENO */
#if !defined(STDOIN_FILENO)
	#define STDIN_FILENO	0
	#define STDOUT_FILENO	1
	#define STDERR_FILENO	2
#endif

typedef struct PluginRef
{
    const AuthorizationCallbacks *callbacks;
} PluginRef;

typedef enum MechanismId
{
    kMechNone,
	kMechCheck,
    kMechUserMount,	
	kMechUMount
} MechanismId;

typedef struct MechanismRef
{
    const PluginRef *plugin;
    AuthorizationEngineRef engine;
    MechanismId mechanismId;
} MechanismRef;

static gid_t macFuseAdminGroup=80;
static const char* fishIconOpt="-ovolicon=/System/Library/CoreServices/SecurityAgentPlugins/EncfsVault.bundle/Contents/Resources/hellfish.icns";

/**
 *   A EncFS non-UI mechanism.
 */
static BOOL checkBuf(const char *buf, size_t bufSize)
{
	return ((bufSize > 1) && (buf[bufSize - 1] == 0));
}

static OSStatus invokeCheck(MechanismRef *mechanism, const AuthorizationValue *uName, const AuthorizationValue *uHome)
{
	NSString *userName = [[NSString alloc ] initWithBytes:uName->data length:(uName->length-1) encoding:NSASCIIStringEncoding ];
	NSString *userHome = [[NSString alloc ] initWithBytes:uHome->data length:(uHome->length-1) encoding:NSASCIIStringEncoding ];
	NSString* description = [NSString stringWithFormat:@"EncfsVault Check:[ home %@ user %@ ]", userHome, userName];
	syslog(LOG_DEBUG, [description UTF8String] );
	sleep( 20 );

	[ userName release ];
	[ userHome release ];

	return kAuthorizationResultAllow;
}

static OSStatus invokeMount(MechanismRef *mechanism, const AuthorizationValue *uUid, const AuthorizationValue *uName, 
							const AuthorizationValue *uPass, const AuthorizationValue *uHome)
{
	NSString *userName = [[NSString alloc ] initWithBytes:uName->data length:(uName->length-1) encoding:NSASCIIStringEncoding ];
	NSString *userPassword = [[NSString alloc ] initWithBytes:uPass->data length:(uPass->length-1) encoding:NSASCIIStringEncoding ];
	NSString *homeDir = [[NSString alloc ] initWithBytes:uHome->data length:(uHome->length-1) encoding:NSASCIIStringEncoding ];
	OSStatus status = kAuthorizationResultAllow;
	
	if(mountFilesystem(*(uid_t*)uUid->data, macFuseAdminGroup, userName, [userPassword UTF8String], homeDir) != 0)
	{
		status = errAuthorizationInternal;
		syslog(LOG_ERR, "EncfsVault: Error encfs mount returned a failure" );
	}
	
	[ userName release ];
	[ userPassword release ];
	[ homeDir release ];
		
	return status;
}

static OSStatus invokeUnmount(MechanismRef *mechanism, const AuthorizationValue *homeDir)
{
	return unmountFilesystem(homeDir);
}

static OSStatus pluginDestroy(AuthorizationPluginRef inPlugin)
{
    PluginRef *plugin = (PluginRef *)inPlugin;
    free(plugin);
    return 0;
}

static OSStatus mechanismCreate(AuthorizationPluginRef inPlugin,
        AuthorizationEngineRef inEngine,
        AuthorizationMechanismId mechanismId,
        AuthorizationMechanismRef *outMechanism)
{
    const PluginRef *plugin = (const PluginRef *)inPlugin;
    MechanismRef *mechanism = calloc(1, sizeof(MechanismRef));

    mechanism->plugin = plugin;
    mechanism->engine = inEngine;
	
    if (!strcmp(mechanismId, "none"))
        mechanism->mechanismId = kMechNone;
    else if (!strcmp(mechanismId, "check"))
        mechanism->mechanismId = kMechCheck;
    else if (!strcmp(mechanismId, "user-mount"))
        mechanism->mechanismId = kMechUserMount;
    else if (!strcmp(mechanismId, "umount"))
        mechanism->mechanismId = kMechUMount;
    else
        return errAuthorizationInternal;

    *outMechanism = mechanism;

    return 0;
}

static OSStatus mechanismInvoke(AuthorizationMechanismRef inMechanism)
{
	MechanismRef *mechanism = (MechanismRef *)inMechanism;
    OSStatus status = kAuthorizationResultAllow;

	if( mechanism->mechanismId != kMechNone )
	{
		const AuthorizationValue *uName, *uPass, *uHome, *uUid;
		AuthorizationContextFlags uNameFlags, uPassFlags, uHomeFlags, uUidFlags;
		
		if((mechanism->plugin->callbacks->GetContextValue(mechanism->engine, kAuthorizationEnvironmentUsername, &uNameFlags, &uName) == noErr) &&
		   (mechanism->plugin->callbacks->GetContextValue(mechanism->engine, kAuthorizationEnvironmentPassword, &uPassFlags, &uPass) == noErr ) &&
		   (mechanism->plugin->callbacks->GetContextValue(mechanism->engine, "home", &uHomeFlags, &uHome) == noErr ) &&
		   (mechanism->plugin->callbacks->GetContextValue(mechanism->engine, "uid", &uUidFlags, &uUid) == noErr ))
		{
			if(checkBuf(uName->data, uName->length) && checkBuf(uPass->data, uPass->length) && checkBuf(uHome->data, uHome->length) &&
					uUid->length == sizeof(uid_t))
			{
				switch (mechanism->mechanismId)
				{
					case kMechCheck:
						status = invokeCheck(mechanism, uName, uHome);
						break;
					case kMechUserMount:
						status = invokeMount(mechanism, uUid, uName, uPass, uHome);
						break;
					case kMechUMount:
						status = invokeUnmount(mechanism, uHome);
						break;
					default:
						break;
				}				
			}
			else
			{
				syslog( LOG_ERR, "EncfsVault: Checksize failure" );
			}
		}
		else
		{
			syslog( LOG_ERR, "EncfsVault: Error getting user info" );
		}
	}
		
    return mechanism->plugin->callbacks->SetResult(mechanism->engine, status);
}

static OSStatus mechanismDeactivate(AuthorizationMechanismRef inMechanism)
{
    return 0;
}

static OSStatus mechanismDestroy(AuthorizationMechanismRef inMechanism)
{
    MechanismRef *mechanism = (MechanismRef *)inMechanism;
    free(mechanism);

    return 0;
}

AuthorizationPluginInterface pluginInterface =
{
    kAuthorizationPluginInterfaceVersion, //UInt32 version;
    pluginDestroy,
    mechanismCreate,
    mechanismInvoke,
    mechanismDeactivate,
    mechanismDestroy
};

OSStatus AuthorizationPluginCreate(const AuthorizationCallbacks *callbacks,
    AuthorizationPluginRef *outPlugin,
    const AuthorizationPluginInterface **outPluginInterface)
{
    PluginRef *plugin = calloc(1, sizeof(PluginRef));

    plugin->callbacks = callbacks;
    *outPlugin = (AuthorizationPluginRef) plugin;
    *outPluginInterface = &pluginInterface;
    return 0;
}

/*
 * EncFS mount functions
 */
static NSString* getPathForEncfs()
{
	NSString* searchPath;
	NSArray* possiblePaths = [NSArray arrayWithObjects:
		@"/usr/local/bin", @"/opt/local/bin", @"/sw/bin", nil];
	NSEnumerator* e = [possiblePaths objectEnumerator];
	while (searchPath = [e nextObject])
	{
		NSString* libraryPath = [searchPath stringByAppendingPathComponent:@"encfs"];
		if ([[NSFileManager defaultManager] fileExistsAtPath:libraryPath])
			return searchPath;
	}
	
	return nil;
}

static BOOL isMounted(NSString * pathStr)
{
/* 
 *  Does not show fuse mounts all the time, user issue 
 *
	BOOL wasMounted = NO;
	NSArray *mountList = [[NSWorkspace sharedWorkspace] mountedLocalVolumePaths];
	NSString *path;
	NSEnumerator *pathEnumerator = [mountList objectEnumerator];
	while (path = [pathEnumerator nextObject])
	{
		if(path == pathStr)
		{
			wasMounted = YES; 
			break;
		}
	}
 *	
 * So just check for magic file dir instead	
 */
	NSString *magicFile= [pathStr stringByAppendingString:@"/.encfsvault"];	
	NSFileManager *fileManager = [NSFileManager defaultManager];
	return [fileManager fileExistsAtPath:magicFile]; 
}

static BOOL validateFilesystem(NSString *sPath)
{
	BOOL encFsFound = NO;
	if (sPath != nil && sPath != @"" || [[sPath stringByStandardizingPath] hasPrefix:@"/"])
	{
		NSString *encFile = [sPath stringByAppendingString:@"/.encfs5"];	
		NSFileManager *fileManager = [NSFileManager defaultManager];
		if ( [fileManager fileExistsAtPath:encFile]) 
		{
			encFsFound = YES;
		}
	}
		
	return encFsFound;
}

int forkEncfsAsUser(uid_t uId, gid_t gId, const char* password, const char* storagePath, const char* mountPath, const char* userName,
				const char* encfsWithPath, const char* volumeOpt)
{
	int status = -1;	
	int forkStatus = 0;
	pid_t pid;

	char *args[] = { (char*)encfsWithPath, "-S", "--idle=1", (char*)storagePath, (char*)mountPath, "--",
						(char*)volumeOpt, "-osubtype=10", "-oping_diskarb", "-oallow_other", (char*)fishIconOpt, (char*)0 };
	
	int pipes[2];
    if (pipe(pipes) == 0)
    {
		pid = vfork();
		if(pid == 0)
		{
			int nullWrite = open("/dev/null", O_WRONLY);
			if(nullWrite > 0)
			{
				dup2(nullWrite, STDOUT_FILENO);
				dup2(nullWrite, STDERR_FILENO);
				close(nullWrite);
			}
			
			dup2(pipes[0], STDIN_FILENO);
			close(pipes[1]);
					
			if(setgid(gId) == 0)
			{
				if (setuid(uId) == 0)
				{
					execv(encfsWithPath, args);
					syslog(LOG_ERR, "EncfsVault: Exec Failed" );
				}
				else
				{
					syslog(LOG_ERR, "EncfsVault: Faild to set uid");
				}
			}
			else
			{
				syslog(LOG_ERR, "EncfsVault: Faild to set gid" );
			}
			_exit (0);
		}
		else if( pid < 0 )
		{
			syslog(LOG_ERR, "EncfsVault: Fork Failed" );
		}
		else
		{
			close(pipes[0]);
			write(pipes[1], password, strlen(password) );
			write(pipes[1], "\n", 1 );
			
			if (waitpid (pid, &forkStatus, 0) != pid)
			{
				forkStatus = -1;
			}
			else
			{
				if(WIFEXITED(forkStatus))
				{
					status = WEXITSTATUS(forkStatus);
				}
			}

			close(pipes[1]);
		}
	}
	else
	{
		syslog(LOG_ERR, "EncfsVault: Error creating pipes" );
	}
		
	return status;
}

int mountFilesystem(uid_t uId, gid_t gId, NSString *userName, const char *password, NSString *homeDir)
{
	int status = 0;
	if (! isMounted(homeDir))
	{
		NSString* encfspath = getPathForEncfs();
		if (encfspath != nil)
		{	
			// Make the storage path from home dir:  /Users/foo will be /Users/.foo
			NSString *pathNoUser = [homeDir stringByDeletingLastPathComponent];
			NSString *pathWithDot = [pathNoUser stringByAppendingString:@"/."];	
			NSString *storagePath = [pathWithDot stringByAppendingString:userName];
			
			if( validateFilesystem( storagePath ) )
			{
				BOOL mountPathAvail = NO;
				BOOL isDir = NO;
				NSFileManager *fileManager = [NSFileManager defaultManager];
				if( [fileManager fileExistsAtPath:homeDir isDirectory:&isDir] )
				{
					if(! isDir )
					{
						status = -1;
						syslog(LOG_ERR, "EncfsVault: Error home dir was a file not a directory" );
					}
					else
					{
						/* Call unmount because encfs timeout will leave a fuse mount mounted but not readable */
						unmount([homeDir UTF8String], 0);
						mountPathAvail = YES;
					}
				}
				
				if( mountPathAvail )
				{
					NSString* volumeOption = [NSString stringWithFormat:@"-ovolname=%@", userName];
					NSString* encfsWithPath = [NSString stringWithString: [getPathForEncfs() stringByAppendingPathComponent:@"encfs"]];
					status = forkEncfsAsUser(uId, gId, password, [storagePath UTF8String], [homeDir UTF8String], [userName UTF8String], 
											 [encfsWithPath UTF8String], [volumeOption UTF8String]);
				}
				else
				{
					syslog(LOG_ERR, "EncfsVault: Error home directory was missing" );
				}
			}
			else
			{
				syslog(LOG_DEBUG, "EncfsVault: Could not find .encfs file in home direcotry" );
			}
		}
		else
		{
			syslog( LOG_ERR, "EncfsVault: Failed to find encfs" );
		}
	}
			   
	return status;
}

int unmountFilesystem(NSString *homeDir)
{
	int status = kAuthorizationResultAllow;	
	if(isMounted(homeDir))
	{
		if(unmount([homeDir UTF8String], 0) == -1)
		{
			NSString* description = [NSString stringWithFormat:@"EncfsVault: Failed to unmount %@", homeDir];
			syslog(LOG_ERR, [description UTF8String] );
		}
	}
		
	return status;
}
 
/*
 * End EncFS Mount Functions
 */